﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using Database.Core.Attributes;
using Database.Core.Exceptions;

namespace Database.Core
{
  /// <summary>
  ///   Class Extensions.
  /// </summary>
  public static class Extensions
  {
    private static readonly Hashtable StringValues = new Hashtable();
    private static readonly Hashtable DescriptionValues = new Hashtable();

    private static readonly object EnumStringLocker = new object();
    private static readonly object EnumDescriptionLocker = new object();

    /// <summary>
    ///   Create an entity from a <see cref="DataRow" />.
    /// </summary>
    /// <typeparam name="T">Entity type.</typeparam>
    /// <param name="dataRow">The <see cref="DataRow" />.</param>
    /// <returns>Populated entity.</returns>
    public static T ToEntity<T>(this DataRow dataRow)
    {
      T entity;

      try
      {
        entity = Activator.CreateInstance<T>();
      }
      catch(Exception ex)
      {
        throw new Exception($"Default (parameterless) constructor does not exist for type: {typeof(T).Name}", ex);
      }

      PopulateEntity.ToEntity(entity, dataRow);

      return entity;
    }

    /// <summary>
    ///   Populates an existing entity from a <see cref="DataRow" />.
    /// </summary>
    /// <typeparam name="T">Entity type</typeparam>
    /// <param name="dataRow">The <see cref="DataRow" />.</param>
    /// <param name="entity">The entity.</param>
    public static void ToEntity<T>(this DataRow dataRow, T entity)
    {
      PopulateEntity.ToEntity(entity, dataRow);
    }

    /// <summary>
    ///   Create an entity from first <see cref="DataRow" /> contained within the <see cref="DataTable" />.
    /// </summary>
    /// <typeparam name="T">Entity type</typeparam>
    /// <param name="dataTable">The <see cref="DataTable" />.</param>
    public static T ToEntity<T>(this DataTable dataTable)
    {
      var entity = default(T);

      if(dataTable.Rows.Count <= 0)
      {
        return entity;
      }

      try
      {
        entity = Activator.CreateInstance<T>();
      }
      catch(Exception ex)
      {
        throw new Exception(
          $"Default (parameterless) constructor does not exist for type: {typeof(T).Name}", ex);
      }

      PopulateEntity.ToEntity(entity, dataTable.Rows[0]);

      return entity;
    }

    /// <summary>
    ///   Populates an existing entity from first <see cref="DataRow" /> contained within the <see cref="DataTable" />.
    /// </summary>
    /// <typeparam name="T">Entity type</typeparam>
    /// <param name="dataTable">The <see cref="DataTable" />.</param>
    /// <param name="entity">The entity.</param>
    public static void ToEntity<T>(this DataTable dataTable, T entity)
    {
      if(dataTable.Rows.Count > 0)
      {
        PopulateEntity.ToEntity(entity, dataTable.Rows[0]);
      }
    }

    /// <summary>
    ///   Create an entity from specified <see cref="DataRow" /> contained within the <see cref="DataTable" />.
    /// </summary>
    /// <typeparam name="T">Entity type</typeparam>
    /// <param name="dataTable">The <see cref="DataTable" />.</param>
    /// <param name="rowIndex">Index of the <see cref="DataRow" />.</param>
    public static T ToEntity<T>(this DataTable dataTable, int rowIndex)
    {
      var entity = default(T);

      if(dataTable.Rows.Count <= rowIndex)
      {
        return entity;
      }

      try
      {
        entity = Activator.CreateInstance<T>();
      }
      catch(Exception ex)
      {
        throw new Exception(
          String.Format("Default (parameterless) constructor does not exist for type: {0}", typeof(T).Name), ex);
      }

      PopulateEntity.ToEntity(entity, dataTable.Rows[rowIndex]);

      return entity;
    }

    /// <summary>
    ///   Populates an existing entity from specified <see cref="DataRow" /> contained within the <see cref="DataTable" />.
    /// </summary>
    /// <typeparam name="T">Entity type</typeparam>
    /// <param name="dataTable">The <see cref="DataTable" />.</param>
    /// <param name="entity">The entity.</param>
    /// <param name="rowIndex">Index of the <see cref="DataRow" />.</param>
    public static void ToEntity<T>(this DataTable dataTable, T entity, int rowIndex)
    {
      if(dataTable.Rows.Count > rowIndex)
      {
        PopulateEntity.ToEntity(entity, dataTable.Rows[rowIndex]);
      }
    }

    /// <summary>
    ///   Creates and populates entities for each <see cref="DataRow" /> contained within the <see cref="DataTable" />.
    /// </summary>
    /// <typeparam name="T">Entity type.</typeparam>
    /// <param name="dataTable">The <see cref="DataTable" />.</param>
    /// <returns><see cref="IEnumerable" /> containing the entities.</returns>
    public static IEnumerable<T> ToEntities<T>(this DataTable dataTable)
    {
      return PopulateEntity.ToEntities<T>(dataTable);
    }

    /// <summary>
    ///   Creates and populates entities for each <see cref="DataRow" /> contained within the first <see cref="DataTable" />.
    /// </summary>
    /// <typeparam name="T">Entity Type</typeparam>
    /// <param name="dataSet">The <see cref="DataSet" />.</param>
    /// <returns><see cref="IEnumerable" /> containing the entities.</returns>
    public static IEnumerable<T> ToEntities<T>(this DataSet dataSet)
    {
      return PopulateEntity.ToEntities<T>(dataSet.Tables[0]);
    }

    /// <summary>
    ///   Creates and populates entities for each <see cref="DataRow" /> contained within the specified
    ///   <see cref="DataTable" />.
    /// </summary>
    /// <typeparam name="T">Entity Type</typeparam>
    /// <param name="dataSet">The <see cref="DataSet" />.</param>
    /// <param name="tableIndex">Index of the table.</param>
    /// <returns><see cref="IEnumerable" /> containing the entities.</returns>
    public static IEnumerable<T> ToEntities<T>(this DataSet dataSet, int tableIndex)
    {
      return PopulateEntity.ToEntities<T>(dataSet.Tables[tableIndex]);
    }

    /// <summary>
    ///   Gets the enum string value.
    /// </summary>
    /// <param name="value">The enum value.</param>
    /// <returns><see cref="System.String" />. value of the <see cref="StringValueAttribute" /></returns>
    public static string GetEnumStringValue(this Enum value)
    {
      string output = null;

      lock(EnumStringLocker)
      {
        if(StringValues.ContainsKey(value))
        {
          var sv = (StringValueAttribute)StringValues[value];
          if(sv != null)
          {
            output = sv.StringValue;
          }
        }
        else
        {
          var type = value.GetType();

          //Look for our 'StringValueAttribute' in the field's custom attributes
          var fi = type.GetField(value.ToString());
          var attrs = fi.GetCustomAttributes(typeof(StringValueAttribute), false) as StringValueAttribute[];

          if(attrs != null
             && attrs.Length > 0)
          {
            // lock the Hash table while inserting new data. 
            // verify value has not been added since first check
            if(!StringValues.ContainsKey(value))
            {
              StringValues.Add(value, attrs[0]);
            }

            output = attrs[0].StringValue;
          }
        }
      }

      return output;
    }

    /// <summary>
    ///   Gets the enum description.
    /// </summary>
    /// <param name="value">The enum value.</param>
    /// <returns><see cref="System.String" />. value of the <see cref="DescriptionAttribute" /></returns>
    public static string GetEnumDescription(this Enum value)
    {
      string output = null;

      lock(EnumDescriptionLocker)
      {
        if(DescriptionValues.ContainsKey(value))
        {
          var sv = (DescriptionAttribute)DescriptionValues[value];
          if(sv != null)
          {
            output = sv.Description;
          }
        }
        else
        {
          var type = value.GetType();

          var fi = type.GetField(value.ToString());
          var attrs = fi.GetCustomAttributes(typeof(DescriptionAttribute), false) as DescriptionAttribute[];

          output = attrs == null
            ? value.ToString()
            : attrs[0].Description;

          if(attrs != null
             && attrs.Length > 0)
          {
            // lock the Hash table while inserting new data. 
            // verify value has not been added since first check
            if(!DescriptionValues.ContainsKey(value))
            {
              DescriptionValues.Add(value, attrs[0]);
            }

            output = attrs[0].Description;
          }
        }
      }

      return output;
    }

    /// <summary>
    ///   Parses the enum string value.
    /// </summary>
    /// <param name="value">The enum value.</param>
    /// <param name="stringValue">The string value.</param>
    /// <param name="ignoreCase">if set to <c>true</c> [ignore case].</param>
    /// <returns>System.Object.</returns>
    public static object StringValueParse(this Enum value, string stringValue, bool ignoreCase = false)
    {
      object output = null;
      string enumStringValue = null;
      var type = value.GetType();

      //Look for our string value associated with fields in this enum
      foreach(var fi in type.GetFields())
      {
        //Check for our custom attribute
        var attrs = fi.GetCustomAttributes(typeof(StringValueAttribute), false) as StringValueAttribute[];
        if(attrs != null
           && attrs.Length > 0)
        {
          enumStringValue = attrs[0].StringValue;
        }

        //Check for equality then select actual enum value.
        if(String.Compare(enumStringValue, stringValue, ignoreCase) == 0)
        {
          output = Enum.Parse(type, fi.Name);
          break;
        }
      }

      return output;
    }

    /// <summary>
    ///   Parses the enum description attribute value.
    /// </summary>
    /// <param name="value">The enum value.</param>
    /// <param name="stringValue">The string value.</param>
    /// <param name="ignoreCase">if set to <c>true</c> [ignore case].</param>
    /// <returns>System.Object.</returns>
    public static object EnumDescriptionParse(this Enum value, string stringValue, bool ignoreCase = false)
    {
      object output = null;
      string enumStringValue = null;
      var type = value.GetType();

      //Look for our string value associated with fields in this enum
      foreach(var fi in type.GetFields())
      {
        //Check for our custom attribute
        var attrs = fi.GetCustomAttributes(typeof(DescriptionAttribute), false) as DescriptionAttribute[];
        if(attrs != null
           && attrs.Length > 0)
        {
          enumStringValue = attrs[0].Description;
        }

        //Check for equality then select actual enum value.
        if(string.Compare(enumStringValue, stringValue, ignoreCase) != 0)
        {
          continue;
        }

        output = Enum.Parse(type, fi.Name);

        break;
      }

      return output;
    }

    /// <summary>
    ///   Determines whether [is string defined] [the specified enum type].
    /// </summary>
    /// <param name="value">Enum value.</param>
    /// <param name="stringValue">The string value.</param>
    /// <param name="ignoreCase">if set to <c>true</c> [ignore case].</param>
    /// <returns><c>true</c> if [is string defined] [the specified enum type]; otherwise, <c>false</c>.</returns>
    public static bool IsStringDefined(this Enum value, string stringValue = null, bool ignoreCase = false)
    {
      if(String.IsNullOrEmpty(stringValue))
      {
        return value.GetType()
          .GetField(value.ToString())
          .GetCustomAttributes(typeof(StringValueAttribute), false).Length > 0;
      }

      return StringValueParse(value, stringValue, ignoreCase) != null;
    }

    /// <summary>
    ///   Determines whether [is description defined] [the specified enum type].
    /// </summary>
    /// <param name="value">Enum value.</param>
    /// <param name="descriptionValue">The description value.</param>
    /// <param name="ignoreCase">if set to <c>true</c> [ignore case].</param>
    /// <returns><c>true</c> if [is description defined] [the specified enum type]; otherwise, <c>false</c>.</returns>
    public static bool IsDescriptionDefined(this Enum value, string descriptionValue = null, bool ignoreCase = false)
    {
      if(string.IsNullOrEmpty(descriptionValue))
      {
        return value.GetType()
          .GetField(value.ToString())
          .GetCustomAttributes(typeof(DescriptionAttribute), false).Length > 0;
      }

      return EnumDescriptionParse(value, descriptionValue, ignoreCase) != null;
    }

    /// <summary>
    ///   Gets the underlying type of the supplied type.
    /// </summary>
    /// <param name="type">The type.</param>
    /// <param name="enumAsInt">if set to <c>true</c> returns enum types as integer.</param>
    /// <returns>Type.</returns>
    /// <exception cref="ArgumentNullException">type</exception>
    /// <exception cref="Database.Core.Exceptions.TypeConversionException">Type Search Failed</exception>
    public static Type GetUnderlyingType(this Type type, bool enumAsInt = false)
    {
      var conversionType = type;

      try
      {
        // Note: This if block was taken from Convert.ChangeType as is, and is needed here since we're
        // checking properties on conversionType below.
        if(conversionType == null)
        {
          throw new ArgumentNullException("type");
        } // end if

        // If it's not a nullable type, just pass through the parameters to Convert.ChangeType
        if(conversionType.IsGenericType
           &&
           conversionType.GetGenericTypeDefinition() == typeof(Nullable<>))
        {
          // It's a nullable type, and not null, so that means it can be converted to its underlying type,
          // so overwrite the passed-in conversion type with this underlying type
          var nullableConverter = new NullableConverter(conversionType);
          conversionType = nullableConverter.UnderlyingType;
        } // end if

        //Is it an Enum?
        if(conversionType.IsEnum)
        {
          conversionType = enumAsInt
            ? typeof(int)
            : typeof(string);
        }
      }
      catch(Exception ex)
      {
        throw new TypeConversionException(type, "Type Search Failed", ex);
      }

      return conversionType;
    }
  }
}
